---
title: 'Implement webhooks from Nango'
sidebarTitle: 'Implement webhooks from Nango'
description: 'Step by step guide to receive webhooks from Nango'
---

## Set up webhooks from Nango

<Note>
    Webhook settings in Nango are specific to each [environment](/guides/platform/environments).
</Note>

To subscribe to Nango webhooks:

1. Set up a `POST` endpoint in your app to receive the Nango webhooks
2. Input the endpoint's URL in your _Environment Settings_, under _Webhook URLs_ ([direct link for your dev environment](https://app.nango.dev/dev/environment-settings#notification))
3. Implement [verify incoming webhooks](#verifying-webhooks-from-nango) to ensure sure only authentic Nango webhooks are processed
4. Implement processing logic for each [webhook type](#types-of-nango-webhooks) from Nango you want to handle
    - Make sure your processing logic handles the webhooks you have enabled in the environment settings

You can configure up to two webhook URLs per environment. Nango webhooks will be sent to both.

To test webhooks locally, use a webhook forwarding service like [ngrok](https://dev.to/mmascioni/testing-and-debugging-webhooks-with-ngrok-4alp), or [webhook.site](https://webhook.site/).

## Verifying webhooks from Nango

Validate webhooks from Nango by looking at the `X-Nango-Hmac-Sha256` header.

It's an HMAC-SHA256 hash of the webhook payload, using the secret key found in the _Environment Settings_ in the Nango UI.

<Warning>
Nango webhook requests include an `X-Nango-Hmac-Sha256` header for secure verification. A legacy `X-Nango-Signature` header (using plain SHA-256) is also sent for backwards compatibility but should not be used. If you're currently using `X-Nango-Signature`, migrate to `X-Nango-Hmac-Sha256` for improved security.
</Warning>

The webhook signature can be generated with the following code:

<Tabs>
  <Tab title="Node SDK">
    ```typescript
    async (req, res) => {
        const isValid = nango.verifyIncomingWebhookRequest(req.body, req.headers);
    }
    ```
  </Tab>
  <Tab title="Javascript/Typescript">
    ```typescript
    import crypto from 'crypto';
    
    const secretKeyDev = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx';
    const body = // raw request body as string
    const hash = crypto.createHmac('sha256', secretKeyDev).update(body).digest('hex');
    ```
  </Tab>
  <Tab title="Python">
    ```python
    import hmac
    import hashlib
    
    secret_key_dev = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
    body = # raw request body as bytes
    hash = hmac.new(secret_key_dev.encode('utf-8'), body, hashlib.sha256).hexdigest()
    ```
  </Tab>
  <Tab title="Java">
    ```java
    import javax.crypto.Mac;
    import javax.crypto.spec.SecretKeySpec;
    import java.nio.charset.StandardCharsets;
    
    public class Main {
        public static void main(String[] args) throws Exception {
            String secretKeyDev = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
            String body = // raw request body as string
    
            Mac mac = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKey = new SecretKeySpec(secretKeyDev.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
            mac.init(secretKey);
            byte[] hashBytes = mac.doFinal(body.getBytes(StandardCharsets.UTF_8));
            
            StringBuilder hexString = new StringBuilder();
            for (byte b : hashBytes) {
                hexString.append(String.format("%02x", b));
            }
            String hash = hexString.toString();
        }
    }
    ```
  </Tab>
  <Tab title="Ruby">
    ```ruby
    require 'openssl'
    
    secret_key_dev = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
    body = # raw request body 
    hash = OpenSSL::HMAC.hexdigest('SHA256', secret_key_dev, body)
    ```
  </Tab>
  <Tab title="Go">
    ```go
    package main
    
    import (
        "crypto/hmac"
        "crypto/sha256"
        "encoding/hex"
        "io"
        "net/http"
    )
    
    func main() {
        secretKeyDev := "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
        var body []byte // raw request body
    
        h := hmac.New(sha256.New, []byte(secretKeyDev))
        h.Write(body)
        hash := hex.EncodeToString(h.Sum(nil))
    }
    ```
  </Tab>
  <Tab title="Rust">
    ```rust
    use hmac::{Hmac, Mac};
    use sha2::Sha256;
    
    type HmacSha256 = Hmac<Sha256>;
    
    let secret_key_dev = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
    let body = // raw request body as string or bytes
    let mut mac = HmacSha256::new_from_slice(secret_key_dev.as_bytes()).unwrap();
    mac.update(body.as_bytes());
    let hash = format!("{:x}", mac.finalize().into_bytes());
    ```
  </Tab>
  <Tab title="PHP">
    ```php
    <?php
    $secretKeyDev = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx';
    $body = // raw request body as string
    $hash = hash_hmac('sha256', $body, $secretKeyDev);
    ?>
    ```
  </Tab>
</Tabs>

Only accept a webhook if the `X-Nango-Hmac-Sha256` header value matches the webhook signature.

## Types of Nango webhooks

<Warning>
  New Nango webhook types are added regularly, without considering this a breaking change. Your webhook handling logic should gracefully support receiving new types of webhooks by simply ignoring them.
</Warning>

All webhooks from Nango are POST requests.

The exact webhook type definitions can be found [here](https://github.com/NangoHQ/nango/blob/master/packages/types/lib/webhooks/api.ts).

### Auth webhooks

New connection webhooks have `"type": "auth"` and `"operation": "creation"`. They are sent after a connection has been successfully created.

Payload received following a connection creation:

```json
{
    "type": "auth",
    "operation": "creation",
    "connectionId": "<your-connection-id>",
    "authMode": "OAUTH2 | BASIC | API_KEY | ...",
    "providerConfigKey": "<your-integration-id>",
    "provider": "<your-provider-key>",
    "environment": "DEV | PROD | ...",
    "success": true,
    "endUser": { 
        "endUserId": "<your-end-user-id>", 
        "tags": { "organizationId": "<your-organization-id>" }
    }
}
```

<Warning>
  Processing webhooks with `"type": "auth"` and `"operation": "creation"` is **necessary**. After a connection is created, these webhooks give you the generated connection ID which lets you access the connection later on.

  Use the values of `endUser.endUserId` (and optionally `endUser.tags`) to reconcile and save the connection ID with the user/org who initiated the connection.
</Warning>

All `authMode` values can be found [here](https://github.com/NangoHQ/nango/blob/master/packages/types/lib/auth/api.ts). The `authMode` value depends on the `provider` value.

All `operation` values are:

- `creation`: a new connection has been created
- `override`: a connection has been re-authorized
- `refresh`: an OAuth connection's access token has failed to refresh

Payload received following a refresh token error:

```json
{
    "type": "auth",
    "operation": "refresh",
    "connectionId": "<your-connection-id>",
    "authMode": "OAUTH2 | BASIC | API_KEY | ...",
    "providerConfigKey": "<your-integration-id>",
    "provider": "<your-provider-key>",
    "environment": "DEV | PROD | ...",
    "success": false,
    "endUser": { 
        "endUserId": "<your-end-user-id>", 
        "tags": { "organizationId": "<your-organization-id>" }
    },
    "error": {
        "type": "<string>",
        "description": "<string>"
    }
}
```

<Note>
  Webhooks are only sent for certain connection creation errors. For example, during the OAuth flow, some errors are reported locally in the OAuth modal by the external API. Since Nango does not receive these errors, it cannot trigger a webhook for them.
</Note>

### Sync webhooks

Sync webhooks are sent when a [sync](/guides/use-cases/syncs) execution finishes, whether successful or not.

Payload received following a successful sync execution:

```json
{
    "type": "sync",
    "connectionId": "<your-connection-id>",
    "providerConfigKey": "<your-integration-id>",
    "syncName": "<your-sync-script-name>",
    "model": "<your-model-name>",
    "syncType": "INCREMENTAL | INITIAL | WEBHOOK",
    "success": true,
    "modifiedAfter": "<timestamp>",
    "responseResults": {
        "added": number,
        "updated": number,
        "deleted": number
    }
}
```

The `modifiedAfter` is an ISO-8601 format string (e.g. `2025-05-21T18:52:49.838Z`) that represents the start time of the last sync.  When the webhook target application receives this payload, it should store this value against the `connectionId` as a "bookmark".  On the next sync webhook event, use the stored bookmark to retrieve the changed records and store the new `modifiedAfter` as the next bookmark.  This mechanism ensures that no deltas are missed.

By default, Nango sends a webhook even if no modified data was detected in the last sync execution (referred as an "empty" sync), but this is configurable in your _Environment Settings_. In case of an empty sync, the `responseResults` would be: 
```json
{
    "added": 0,
    "updated": 0,
    "deleted": 0
}
```

The `syncType` possible values are:

- `INITIAL`: for the very first execution of a sync.
- `INCREMENTAL`: for subsequent executions  of a sync.
- `WEBHOOK`: when an external API webhooks triggers the execution of the sync's `onWebhookPayloadReceived` function.

Payload received following a failed sync execution:

```json
{
    "type": "sync",
    "connectionId": "<your-connection-id>",
    "providerConfigKey": "<your-integration-id>",
    "syncName": "<your-sync-script-name>",
    "model": "<your-model-name>",
    "syncType": "INCREMENTAL | INITIAL | WEBHOOK",
    "success": true,
    "error": {
        "type": "<string>",
        "description": "<string>"
    },
    "startedAt": "<timestamp>",
    "failedAt": "<timestamp>"
}
```

### External webhook forwarding

Webhooks forward from external APIs have the following JSON body:

```json
{
   "from": "hubspot",
   "type": "forward",
   "connectionId": "<CONNECTION-ID>",
   "providerConfigKey": "<INTEGRATION-ID>",
   "payload": ... // Raw payload from Hubspot webhook
}
```

## Webhook retries & debugging

Nango retries each webhook with non-2xx responses 2 times with exponential backoff (starting delay: 100ms, time multiple: 2, view details in the [code](https://github.com/NangoHQ/nango/blob/master/packages/webhooks/lib/utils.ts)).

Webhooks time out after 20 seconds.

Each webhook attempt is logged in Nango's [logs](/guides/platform/logs).

You can also use the [OpenTelemetry exporter](/guides/platform/logs#opentelemetry-logs-export) to monitor Nango webhooks in your own observability stack.

<Tip>
  **Questions, problems, feedback?** Please reach out in the [Slack community](https://nango.dev/slack).
</Tip>